{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "af0d5042",
   "metadata": {},
   "source": [
    "# Multi Investment Optimization\n",
    "\n",
    "In the following, we show how PyPSA can deal with multi-investment optimization, also known as multi-horizon optimization. \n",
    "\n",
    "Here, the total set of snapshots is divided into investment periods. For the model, this translates into multi-indexed snapshots with the first level being the investment period and the second level the according time steps. In each investment period new asset may be added to the system. On the other hand assets may only operate as long as allowed by their lifetime.\n",
    "\n",
    "In contrast to the ordinary optimisation, the following concepts have to be taken into account. \n",
    "\n",
    "1. `investment_periods` - `pypsa.Network` attribute. This is the set of periods which specify when new assets may be built. In the current implementation, these have to be the same as the first level values in the `snapshots` attribute.\n",
    "2. `investment_period_weightings` - `pypsa.Network` attribute. These specify the weighting of each period in the objective function. \n",
    "3. `build_year` - general component attribute. A single asset may only be built when the build year is smaller or equal to the current investment period. For example, assets with a build year `2029` are considered in the investment period `2030`, but not in the period `2025`.  \n",
    "4. `lifetime` - general component attribute. An asset is only considered in an investment period if present at the beginning of an investment period. For example, an asset with build year `2029` and lifetime `30` is considered in the investment period `2055` but not in the period `2060`.   \n",
    "\n",
    "In the following, we set up a three node network with generators, lines and storages and run a optimisation covering the time span from 2020 to 2050 and each decade is one investment period."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f5e767ac",
   "metadata": {},
   "outputs": [],
   "source": [
    "import pypsa\n",
    "import pandas as pd\n",
    "import numpy as np\n",
    "import matplotlib.pyplot as plt"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "95e62e96",
   "metadata": {},
   "source": [
    "We set up the network with investment periods and snapshots. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "bd8cdd68",
   "metadata": {},
   "outputs": [],
   "source": [
    "n = pypsa.Network()\n",
    "years = [2020, 2030, 2040, 2050]\n",
    "freq = \"24\"\n",
    "\n",
    "snapshots = pd.DatetimeIndex([])\n",
    "for year in years:\n",
    "    period = pd.date_range(\n",
    "        start=\"{}-01-01 00:00\".format(year),\n",
    "        freq=\"{}H\".format(freq),\n",
    "        periods=8760 / float(freq),\n",
    "    )\n",
    "    snapshots = snapshots.append(period)\n",
    "\n",
    "# convert to multiindex and assign to network\n",
    "n.snapshots = pd.MultiIndex.from_arrays([snapshots.year, snapshots])\n",
    "n.investment_periods = years\n",
    "\n",
    "n.snapshot_weightings"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "84d2d14f",
   "metadata": {},
   "outputs": [],
   "source": [
    "n.investment_periods"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ba52e3eb",
   "metadata": {},
   "source": [
    "Set the years and objective weighting per investment period. For the objective weighting, we consider a discount rate defined by \n",
    "$$ D(t) = \\dfrac{1}{(1+r)^t} $$ \n",
    "\n",
    "where $r$ is the discount rate. For each period we sum up all discounts rates of the corresponding years which gives us the effective objective weighting."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e0a025d2",
   "metadata": {},
   "outputs": [],
   "source": [
    "n.investment_period_weightings[\"years\"] = list(np.diff(years)) + [10]\n",
    "\n",
    "r = 0.01\n",
    "T = 0\n",
    "for period, nyears in n.investment_period_weightings.years.items():\n",
    "    discounts = [(1 / (1 + r) ** t) for t in range(T, T + nyears)]\n",
    "    n.investment_period_weightings.at[period, \"objective\"] = sum(discounts)\n",
    "    T += nyears\n",
    "n.investment_period_weightings"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ae5e0f16",
   "metadata": {},
   "source": [
    "Add the components"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4d90f0f0",
   "metadata": {},
   "outputs": [],
   "source": [
    "for i in range(3):\n",
    "    n.add(\"Bus\", \"bus {}\".format(i))\n",
    "\n",
    "# add three lines in a ring\n",
    "n.add(\n",
    "    \"Line\",\n",
    "    \"line 0->1\",\n",
    "    bus0=\"bus 0\",\n",
    "    bus1=\"bus 1\",\n",
    ")\n",
    "\n",
    "n.add(\n",
    "    \"Line\",\n",
    "    \"line 1->2\",\n",
    "    bus0=\"bus 1\",\n",
    "    bus1=\"bus 2\",\n",
    "    capital_cost=10,\n",
    "    build_year=2030,\n",
    ")\n",
    "\n",
    "n.add(\n",
    "    \"Line\",\n",
    "    \"line 2->0\",\n",
    "    bus0=\"bus 2\",\n",
    "    bus1=\"bus 0\",\n",
    ")\n",
    "\n",
    "n.lines[\"x\"] = 0.0001\n",
    "n.lines[\"s_nom_extendable\"] = True"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "804e7b53",
   "metadata": {},
   "outputs": [],
   "source": [
    "n.lines"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ccdca3cd",
   "metadata": {},
   "outputs": [],
   "source": [
    "# add some generators\n",
    "p_nom_max = pd.Series(\n",
    "    (np.random.uniform() for sn in range(len(n.snapshots))),\n",
    "    index=n.snapshots,\n",
    "    name=\"generator ext 2020\",\n",
    ")\n",
    "\n",
    "# renewable (can operate 2020, 2030)\n",
    "n.add(\n",
    "    \"Generator\",\n",
    "    \"generator ext 0 2020\",\n",
    "    bus=\"bus 0\",\n",
    "    p_nom=50,\n",
    "    build_year=2020,\n",
    "    lifetime=20,\n",
    "    marginal_cost=2,\n",
    "    capital_cost=1,\n",
    "    p_max_pu=p_nom_max,\n",
    "    carrier=\"solar\",\n",
    "    p_nom_extendable=True,\n",
    ")\n",
    "\n",
    "# can operate 2040, 2050\n",
    "n.add(\n",
    "    \"Generator\",\n",
    "    \"generator ext 0 2040\",\n",
    "    bus=\"bus 0\",\n",
    "    p_nom=50,\n",
    "    build_year=2040,\n",
    "    lifetime=11,\n",
    "    marginal_cost=25,\n",
    "    capital_cost=10,\n",
    "    carrier=\"OCGT\",\n",
    "    p_nom_extendable=True,\n",
    ")\n",
    "\n",
    "# can operate in 2040\n",
    "n.add(\n",
    "    \"Generator\",\n",
    "    \"generator fix 1 2040\",\n",
    "    bus=\"bus 1\",\n",
    "    p_nom=50,\n",
    "    build_year=2040,\n",
    "    lifetime=10,\n",
    "    carrier=\"CCGT\",\n",
    "    marginal_cost=20,\n",
    "    capital_cost=1,\n",
    ")\n",
    "\n",
    "n.generators"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5025e810",
   "metadata": {},
   "outputs": [],
   "source": [
    "n.add(\n",
    "    \"StorageUnit\",\n",
    "    \"storageunit non-cyclic 2030\",\n",
    "    bus=\"bus 2\",\n",
    "    p_nom=0,\n",
    "    capital_cost=2,\n",
    "    build_year=2030,\n",
    "    lifetime=21,\n",
    "    cyclic_state_of_charge=False,\n",
    "    p_nom_extendable=False,\n",
    ")\n",
    "\n",
    "n.add(\n",
    "    \"StorageUnit\",\n",
    "    \"storageunit periodic 2020\",\n",
    "    bus=\"bus 2\",\n",
    "    p_nom=0,\n",
    "    capital_cost=1,\n",
    "    build_year=2020,\n",
    "    lifetime=21,\n",
    "    cyclic_state_of_charge=True,\n",
    "    cyclic_state_of_charge_per_period=True,\n",
    "    p_nom_extendable=True,\n",
    ")\n",
    "\n",
    "n.storage_units"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "694ffb04",
   "metadata": {},
   "source": [
    "Add the load"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7981acdf",
   "metadata": {},
   "outputs": [],
   "source": [
    "load_var = pd.Series(\n",
    "    100 * np.random.rand(len(n.snapshots)), index=n.snapshots, name=\"load\"\n",
    ")\n",
    "n.add(\"Load\", \"load 2\", bus=\"bus 2\", p_set=load_var)\n",
    "\n",
    "load_fix = pd.Series(75, index=n.snapshots, name=\"load\")\n",
    "n.add(\"Load\", \"load 1\", bus=\"bus 1\", p_set=load_fix)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "09467ca4",
   "metadata": {},
   "source": [
    "Run the optimization"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cb271473",
   "metadata": {},
   "outputs": [],
   "source": [
    "n.loads_t.p_set"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "04e5db92",
   "metadata": {},
   "outputs": [],
   "source": [
    "n.optimize(multi_investment_periods=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a5de32ee",
   "metadata": {},
   "outputs": [],
   "source": [
    "c = \"Generator\"\n",
    "df = pd.concat(\n",
    "    {\n",
    "        period: n.get_active_assets(c, period) * n.df(c).p_nom_opt\n",
    "        for period in n.investment_periods\n",
    "    },\n",
    "    axis=1,\n",
    ")\n",
    "df.T.plot.bar(\n",
    "    stacked=True,\n",
    "    edgecolor=\"white\",\n",
    "    width=1,\n",
    "    ylabel=\"Capacity\",\n",
    "    xlabel=\"Investment Period\",\n",
    "    rot=0,\n",
    "    figsize=(10, 5),\n",
    ")\n",
    "plt.tight_layout()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ebd9a17b",
   "metadata": {},
   "outputs": [],
   "source": [
    "df = n.generators_t.p.sum(axis=0).T\n",
    "df.T.plot.bar(\n",
    "    stacked=True,\n",
    "    edgecolor=\"white\",\n",
    "    width=1,\n",
    "    ylabel=\"Generation\",\n",
    "    xlabel=\"Investment Period\",\n",
    "    rot=0,\n",
    "    figsize=(10, 5),\n",
    ")"
   ]
  }
 ],
 "metadata": {
  "@webio": {
   "lastCommId": null,
   "lastKernelId": null
  },
  "kernelspec": {
   "display_name": "",
   "language": "python",
   "name": ""
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
